<?php
/** @noinspection PhpExpressionResultUnusedInspection */

/** @noinspection PhpUndefinedVariableInspection */

namespace Dreamcat\Components\QuickMock\Factory;

use Dreamcat\Components\QuickMock\Error\MockFactoryException;
use Dreamcat\Components\QuickMock\MockFactory;
use Dreamcat\Components\QuickMock\Proxy\ProxyInterface;
use ReflectionClass;
use ReflectionMethod;

/**
 * 快速mock工厂
 * @author vijay
 */
class QuickMockFactory implements MockFactory
{
    /**
     * @inheritDoc
     */
    public function createMock($interfaceName, ProxyInterface $proxyClass)
    {
        if (!interface_exists($interfaceName)) {
            throw new MockFactoryException(
                "interface not exists [{$interfaceName}]",
                MockFactoryException::INTERFACE_NOT_EXISTS
            );
        }
        $code = $this->generateCode($interfaceName, $mockClassName);
        eval($code);
        if (!class_exists($mockClassName, false)) {
            // @codeCoverageIgnoreStart
            throw new MockFactoryException(
                "interface [{$interfaceName}] mock failed", MockFactoryException::EVAL_FAILED
            );
            // @codeCoverageIgnoreEnd
        }
        return new $mockClassName($proxyClass);
    }

    /**
     * 生成实现接口的代码
     * @param string $interfaceName 接口名
     * @param string $mockClassName 类名
     * @return string
     * @noinspection PhpDocMissingThrowsInspection
     */
    private function generateCode($interfaceName, &$mockClassName)
    {
        /** @noinspection PhpUnhandledExceptionInspection */
        $ref = new ReflectionClass($interfaceName);
        # 生成随机类名
        do {
            $mockClassName = $ref->getShortName() . "_mockClass_" . uniqid();
        } while (class_exists($mockClassName));

        # 机构函数
        $code = <<<PHP
class {$mockClassName} implements {$ref->getName()}
{
    private \$proxy;

    public function __construct(\$proxy)
    {
        \$this->proxy = \$proxy;
    }

PHP;

        # 各方法
        foreach ($ref->getMethods() as $method) {
            $code .= "\t" . $method->getDocComment() . "\n\t";
            if ($method->isStatic()) {
                throw new MockFactoryException(
                    "method [{$interfaceName}::{$method->getName()}] is static not support mock",
                    MockFactoryException::STATIC_METHOD_NOT_SUPPORT
                );
            }
            # 接口所有方法一定是public，所以不做判断
            $code .= "public function {$method->getName()}";
            $code .= $this->methodParamCode($method);
            $code .= "\n\t{\n\t\treturn \$this->proxy->invoke(\$this, new \ReflectionMethod(__CLASS__, __FUNCTION__), func_get_args());\n\t}\n";
        }
        return $code . "\n}";
    }

    /**
     * -
     * @param ReflectionMethod $method
     * @return string
     * @noinspection PhpDocMissingThrowsInspection
     */
    private function methodParamCode(ReflectionMethod $method)
    {
        $params = [];
        foreach ($method->getParameters() as $parameter) {
            $pstr = "\${$parameter->getName()}";
            if ($parameter->isVariadic()) {
                $pstr = "...{$pstr}";
            }
            if ($parameter->isPassedByReference()) {
                $pstr = "&{$pstr}";
            }
            if ($parameter->isCallable()) {
                $pstr = "callable {$pstr}";
            } elseif ($parameter->getClass()) {
                $pstr = "{$parameter->getClass()->getName()} {$pstr}";
            } elseif (method_exists($parameter, "hasType") && $parameter->hasType()) {
                /** @noinspection PhpPossiblePolymorphicInvocationInspection */
                $pstr = "{$parameter->getType()->getName()} {$pstr}";
            }
            if ($parameter->isDefaultValueAvailable()) {
                /** @noinspection PhpUnhandledExceptionInspection */
                $pstr = "{$pstr} = " . var_export($parameter->getDefaultValue(), true);
            }
            $params[] = $pstr;
        }
        $code = "(" . implode(", ", $params) . ")";

        if (!method_exists($method, "hasReturnType") || !$method->hasReturnType()) {
            return $code;
        }
        $code .= " :";
        $returnType = $method->getReturnType();
        if ($returnType->allowsNull()) {
            $code .= "?";
        }
        /** @noinspection PhpPossiblePolymorphicInvocationInspection */
        $code .= "{$returnType->getName()}";
        return $code;
    }
}

# end of file
